<?php
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * Passport Trait
 * 
 * @desc    登录特性，可通过重写方法的方式，定制不同客户端功能。如重写 check_login()。
 * @version 1.0.0 
 * @author  Jason
 * @date    2017年6月21日 下午12:24:03
 * 
 * @desc    规范命名，合理使用 $this->session！
 * @version 2.0.0 
 * @author  Jason
 * @date    2017年7月25日 下午4:17:15
 * 
 * @desc    增加【设置登录的用户信息】的接口，用于 PassportLibrary 登录成功之后的登录凭证保存
 * @version 2.1.0 
 * @author  Jason
 * @date    2017年7月25日 下午4:54:29
 * @desc    修复：将接口【设置登录的用户信息】改为【构造介质为 session 形式的登录用户信息】
 *               因为登录凭证的保存是持续的，全局的，换言之是需要介质去做持久化的。
 * @version 2.1.1
 * @author  Jason
 * @date	2017年7月25日 下午5:41:00
 */
trait Passport_Trait
{
	protected $logined_member = null;
	
	/**
	 * session library handle, the object name to assign to, for loader, auto assign.
	 *
	 * @desc Must be public!!! in Any Controller!
	 * @var CI_Session
	 */
	public $session = null;
	
	/** @var Lib_passport */
	public $lib_passport = null;
	
	/**
	 * init 函数
	 * 
	 * @version	2.0.0
	 * @desc	支持传入 session 配置，不同的派生类可以拥有独立的会话
	 * @desc    函数 __construct() 属于魔法函数（不同于普通函数），但并没有魔法函数的实际功能（主调类中use时不会自动调用，也不能new实例）。
	 * 			new Passport_Trait(); -> Fatal error: Cannot instantiate trait Passport_Trait in __FILE__ on line __LINE__
	 * @author  Jason
	 * @date    2017年6月22日 下午1:55:41
	 * 
	 * @param   array $session_params	session 配置
	 * 
	 * @return  void
	 */
	public function __construct(array $session_params = [])
	{
		# parent::__construct();
		
		// $session_params = [
		// 		// MARK 想通过 cookie_path 来区分
		// 		# 'cookie_path' => '/business', // OR 
		// 		# 'cookie_path' => '/chinese/business',
		// 		'cookie_name' => $this->config->item('business_session_key'),
		// 		'cookie_name' => $this->config->item('manager_session_key'),
		// ];
		$this->load->library('lib_cookie');
		$this->load->library('session', $session_params);
		
		// Fatal error: Call to a member function login() on null
		// in /home/bm/web/discovery.segway/code/application/controllers/Passport.php
		// on line 39
		// $this->load->library('lib_passport', null, 'pp');
		// $passport = $this->pp;
		$this->load->library('lib_passport');
		
		// $this->load_member_from_cookie_token();
		$this->load_member_from_session();
		
		$this->access_passport_handler($this->nowCtl, $this->nowAct, $this->nowApp);
	}
	
	/**
	 * 从 COOKIE 中提取 TOKEN, 然后获取登录用户
	 */
	protected function load_member_from_cookie_token()
	{
		$tokenString = $this->lib_cookie->get_token();
		$member = $this->lib_passport->get_member_by_token($tokenString);
		$this->logined_member = $member;
	}
	protected function cookie_token_construct_handler($token)
	{
		// 设置 COOKIE
		$this->lib_cookie->set_token($token);
	}
	protected function cookie_token_destruct_handler()
	{
		$this->lib_cookie->set_token('', time() - 1);
	}
	
	protected function load_member_from_session()
	{
		$this->logined_member = $this->session->LOGINED_MEMBER;
	}
	
	/**
	 * 构造介质为 session 形式的登录用户信息
	 *
	 * @param array $LOGINED_MEMBER
	 *
	 * @desc	用于 PassportLibrary 登录成功之后的登录凭证保存
	 * @author	Jason
	 * @date	2017年7月25日 下午5:39:56
	 */
	protected function session_construct_handler($LOGINED_MEMBER)
	{
		$this->session->LOGINED_MEMBER = $LOGINED_MEMBER;
	}
	
	/**
	 * 删除 Session 数据 并 销毁 SESSION
	 */
	protected function session_destruct_handler()
	{
		// 删除 Session 数据
		$this->session->LOGINED_MEMBER = null;
		unset($_SESSION['LOGINED_MEMBER']);
		// 销毁 SESSION
		$this->session->sess_destroy();
	}
	
	/**
	 * 用户是否已登录
	 *
	 * @param boolean
	 */
	protected function has_member_login()
	{
		return !empty($this->logined_member);
	}
	
	/**
	 * 设置登录的用户信息
	 *
	 * @param array $member
	 * 
	 * @desc	用于 PassportLibrary 登录成功之后的登录凭证保存、之后的同步！
	 * @author	Jason
	 * @date	2017年7月25日 下午4:51:22
	 * @date	2017年7月25日 下午5:48:52
	 */
	public function set_logined_member($member)
	{
		return $this->logined_member = $member;
	}
	
	/**
	 * 获取登录的用户信息
	 *
	 * @param array $member
	 * 
	 * @author	Jason
	 * @desc	开放访问权限，原来 protected 可以在子类中顺利调用，但是如果在 library 里面去访问就不行了！
	 * @date	2017年7月25日 下午4:16:29
	 */
	public function get_logined_member()
	{
		// MARK 返回 null 与返回空数组的不同：前者可直接把结果做下标操作和指针操作，均返回 NULL。重要的是，不报错！
		return $this->has_member_login() ? $this->logined_member : null;
	}
	
	/**
	 * 检测用户是否登录
	 * 
	 * @desc    可通过重写方法的方式，定制不同客户端功能
	 * @author  Jason
	 * @date    2017年6月21日 下午12:39:41
	 * 
	 * @param   type variable
	 * 
	 * @return  boolean	已登录返回真，未登录返回假。
	 */
	protected function check_login()
	{
		if (!$this->has_member_login()) {
			return false;
		}
		
		return true;
	}
	
	protected function access_passport_handler($nowCtl, $nowAct, $nowApp = null)
	{
		$not_auth_login = $this->notAuthLogin($nowCtl, $nowAct, $nowApp);
		
		// 需要强制验证用户身份的 试图拦截请求
		// 检测管理员是否需要及已经登录 试图做拦截
		// 检测商户是否需要及已经二次登录 试图做拦截
		if($not_auth_login) {
			return true;
		}
		// 检测用户是否登录
		// 检测管理员是否登录
		// 检测商户是否登录
		$passed = $this->check_login();
		if (!$passed) {
			// TO DO go to login
			$this->load->helper('url');
			$uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING');
			$current_url = current_url() . '?' . $uri;
			redirect(site_url($nowApp.'passport/login') . "?back_url=" . urlencode($current_url));
				
			// return; 不继续执行，anyway！
			return false;
		}
	}
	
	/**
	 * 判断是否不需要验证登录
	 *
	 * @param string $nowCtl module
	 * @param string $nowAct view
	 * @param string $nowApp
	 *
	 * @return bool
	 */
	private function notAuthLogin($nowCtl, $nowAct, $nowApp = null)
	{
		$this->config->load('trigger', true);
		$nowApp = $nowApp ? rtrim($nowApp, '/') : 'default';
		$appConfig = (array) $this->config->item($nowApp, 'trigger');
		
		$notAuthLoginCtl = (array) @$appConfig['notAuthLoginCtl'];
		$butAuthLoginAct = (array) @$appConfig['butAuthLoginAct'];
		$notAuthLoginAct = (array) @$appConfig['notAuthLoginAct'];
		if (in_array($nowCtl, $notAuthLoginCtl) && !in_array($nowAct, $butAuthLoginAct)) {
			return true;
		}
		if (in_array($nowAct, $notAuthLoginAct)) {
			return true;
		}
		
		return false;
	}
	
}

/**
 * Permission Trait
 *
 * @desc    权限特性
 * @version 1.0.0
 * @author  Jason
 * @date	2017年6月21日 下午12:55:07
 */
trait Permission_Trait
{
	/** @var Lib_permission */
	public $lib_permission = null;
	
	public function __construct()
	{
		# parent::__construct();
		
		$this->load->library('lib_permission');
		
		// 前触发一般包括：
		// 登录验证 - 由 Passport_Trait 来完成
		// 权限验证 - 本地完成
		$this->access_permission_handler($this->nowCtl, $this->nowAct, $this->nowApp);
	}
	
	protected function access_permission_handler($nowCtl, $nowAct, $nowApp = null)
	{
		// 需要强制验证用户身份的 试图拦截请求 - 应该已经检测过！
		// 检测用户是否登录
			// TO DO go to login
			// return; 不继续执行，anyway！
			
		// 检测管理员是否需要及已经登录 试图做拦截 - 应该已经检测过！
		// 检测管理员是否登录
			// TO DO go to manager login
			// return; 不继续执行，anyway！
			
		// 检测商户是否需要及已经二次登录 试图做拦截 - 应该已经检测过！
		// 检测商户是否登录
			// TO DO go to business login
			// return; 不继续执行，anyway！
			
		
		// TO DO check permission
		$not_auth_permission = $this->notAuthPermission($nowApp);
		if ($not_auth_permission) {
			return true;
		}
		
		$action = $this->getParticularPermission($nowCtl, $nowAct, $nowApp);
		if ($action === false) {
			return true;
		}
		
		// MARK required Passport_Trait::get_logined_member()
		$member = $this->get_logined_member();
		$passed = $this->lib_permission->check_access($member, $action);
		if (!$passed) {
			show_error('No Right To Access', 500);
			// return; 不继续执行，anyway！
			return false;
		}
	}
	
	/**
	 * 判断是否不需要验证登录用户的权限
	 *
	 * @desc    从配置文件中获取是否需要验证权限的标志，默认false，需要验证！
	 * @author  Jason
	 * @date    2017年5月11日 上午10:53:29
	 *
	 * @param string $nowApp
	 *
	 * @return bool
	 */
	private function notAuthPermission($nowApp = null)
	{
		$this->config->load('trigger', true);
		$nowApp = $nowApp ? rtrim($nowApp, '/') : 'default';
		$appConfig = (array) $this->config->item($nowApp, 'trigger');
		
		$notAuthPermission = isset($appConfig['needAuthPermission']) ?
					! boolval($appConfig['needAuthPermission']) : false;
		
		return $notAuthPermission;
	}
	
	private function getParticularPermission($nowCtl, $nowAct, $nowApp = null)
	{
		$this->config->load('trigger', true);
		$nowApp = $nowApp ? rtrim($nowApp, '/') : 'default';
		$appConfig = (array) $this->config->item($nowApp, 'trigger');
		
		$permission = null;
		if (is_array(@$appConfig['particularPermission'])) {
			foreach ($appConfig['particularPermission'] as $route => $code) {
				// 后者优先级高（后者覆盖前者）
				// 如果可以忍受倒着写的思维，把具体的规则放前面，那么可以把下面两句对调，并加上return提前返回，性能会稍稍提高。
				if ($route === $nowCtl) {
					$permission = $code;
				}
				if ($route === $nowAct) {
					$permission = $code;
				}
			}
		}
		
		return $permission;
	}
	
}

/**
 * I18N_Trait
 * 
 * @desc    Language | Currency | Timezone
 * @version 1.0.0 
 * @author  Jason
 * @date    2017年6月21日 下午2:41:05
 */
trait I18N_Trait
{
	//protected $language = 'english';
	// TODO TMP: 基类中 COOKIE set_language 后并不能立即生效！COOKIE get_language 可先去取 $this->language，立即生效！
	public $language = 'english'; // lib 调用
	protected $currency = 'USD';
	
	/** @var Lib_language */
	public $lib_language = null;
	
	public function __construct()
	{
		# parent::__construct();
		
		$this->load->library('lib_cookie');
		$this->load->library('lib_language');
		
		$this->language_handler();
		$this->currency_handler();
	}
	
	protected function language_handler()
	{
		// $default = 'english';
		$default = $this->config->item('language');
		
		$cookie_lang = $this->lib_cookie->get_language();
		
		/*
		 * 语言的逻辑：
		 * 提交的GET参数可能会覆盖原有的COOKIE
		 * 前提是，前提是，前提是GET参数的合法的，COOKIE的数据也是合法的，否则都会走默认！
		 */
		$user_lang = $this->input->get('lang');//不能是language,特定接口没法接参数！
		
		// 设置 language
		// $language = $user_lang ? $user_lang : $cookie_lang;
		// $language = $language ? $language : $default;
		$language = $this->lib_language->get_supported_language($user_lang, $cookie_lang);
		
		// 设置 cookie
		$this->haveNewCookieLanguage = false;
		if ($cookie_lang !== $language) {
			$this->lib_cookie->set_language($language);
			$this->haveNewCookieLanguage = true;
		}
		// 篡改 base_url
		if ($language !== $default) {
			//$base_url = $this->config->item('base_url');
			$base_url = $this->config->item('base_url_base');
			$this->config->set_item('base_url', $base_url."$language/");
		}
		
		$this->config->set_item('language', $language);
		// $this->load->language('site');
		$this->lang->load(['common', 'site', 'language', 'passport']);
		
		
		$this->language = $language;
	}
	
	protected function currency_handler()
	{
		$currency = $this->lib_cookie->get_currency();
		
		if (empty($currency)) {
			$default = 'USD';
			$currency = $default;
			$this->lib_cookie->set_currency($currency);
		}
		
		$this->currency = $currency;
	}
	
}

/**
 * View Trait
 *
 * @desc    视图相关特性
 * @version 1.0.0
 * @author  Jason
 * @date	2017年6月21日 下午2:55:08
 */
trait View_Trait
{
	/** @var Lib_section */
	public $lib_section = null;
	
	public function __construct()
	{
		# parent::__construct();
		
		$this->load->library('lib_section');
		
		// 公共模板变量
		$this->view_handler();
	}
	
	protected function view_handler()
	{
		$domain = $this->lib_section->common_domain();
		$head = $this->lib_section->common_head();
		$header = $this->lib_section->common_header();
		$cookie = $this->lib_section->common_cookie();
		
		$header_data = [
				'domain'     => $domain,
				'head'       => $head,
				'header'     => $header,
				'cookie'     => $cookie,
		];
		
		$this->load->vars($header_data);
	}
	
	/**
	 * 设置可变的模板变量
	 *
	 * @desc    description
	 * @author  Jason
	 * @date    2017年5月16日 下午7:21:04
	 *
	 * @param   string $key_path 用 "." 分隔的键路径 eg: "head.title"
	 * @param   mixed  $value    模板变量值
	 *
	 * @return  null
	 */
	protected function set_mutable_view_var($key_path, $value)
	{
		$keys = array_values(array_filter(explode('.', $key_path), 'strlen'));
		if (count($keys) <= 0) {
			return true;
		}
		
		$container_key = array_shift($keys);
		$container = $this->load->get_var($container_key);
		
		$temp =& $container;
		foreach ($keys as $index => $key) {
			if (empty($temp[$key])) {
				$temp[$key] = [];
			}
			else if ($index < count($keys) - 1 && !is_array($temp[$key])) {
				// [^1]1st [^1]2nd [^1]3rd or th
				log_message('error', sprintf("The %dst/nd/rd/th Floor '%s' of KeyPath '%s' is NOT an Array!", $index + 2, $key, $key_path));
				// 指针深入过程中，不是数组不能继续
				return false;
			}
			$temp =& $temp[$key];
		}
		$temp = $value;
		unset($temp);
		
		$this->load->vars($container_key, $container);
		
		return true;
	}
	
}


/**
 * MY_Controller Class
 *
 * MY_Controller 模拟触发器
 * 作为父类，做控制器的触发器用
 * 
 * @desc	重构控制器基类：将各个小功能模块拆分，然后重组成不同的父类控制器
 * @version	2.0.0
 * @author	Jason
 * @date	2017年6月21日 下午4:25:36
 */
class MY_Controller extends CI_Controller
{
	protected $nowApp = '$directory';
	protected $nowCtl = '$class';
	protected $nowAct = '$class/$method';
	
	# /** @var Lib_passport */
	# public $lib_passport = null;
	// MARK 开发调试过程中发现一个关于 Trait 诡异的问题：重定义变量不能发生 trait 和 use trait 的本类中，但是子类派生类是可以的！
	// 		比如，下面这个 Library 已经在 trait Passport_Trait 中定义过，所以不能在本类重新定义！
	// 		但是，但是，但是，可以在其任何子类及其派生类中“重定义”，比如 MY_Business_Controller、MY_Manager_Controller、Passport、Merchant、……
	// 否则，（如果在本类重新定义）会遭遇如下错误：
	// <h4>A PHP Error was encountered</h4>
	// <p>Severity: Runtime Notice</p>
	// <p>Message:  MY_Controller and Passport_Trait define the same property ($lib_passport) in the composition of MY_Controller. This might be incompatible, to improve maintainability consider using accessor methods in traits instead. Class was composed</p>
	// <p>Filename: core/MY_Controller.php</p>
	// <p>Line Number: 505</p>
	
	/** @var Lib_cookie */
	public $lib_cookie = null;
	
	// MARK 引入特性，并定义初始化函数别名，并期望 PHP 后续版本能够自动处理之。
	use Passport_Trait {
		Passport_Trait::__construct as __constructPassport;
	}
	use Permission_Trait {
		Permission_Trait::__construct as __constructPermission;
	}
	use View_Trait {
		View_Trait::__construct as __constructView;
	}
	use I18N_Trait {
		I18N_Trait::__construct as __constructI18N;
	}
	
	
	public function __construct()
	{
		parent::__construct();
		
		$this->load->library('lib_json');
		$this->load->library('lib_cookie');
		$this->load->library('lib_language');
		
// 		// MARK 注意特性初始化函数的调用次序，看来——可能就是因此，PHP 不自动处理之？？？
// 		#Session_Trait::__construct();
// 		// 这个的调用次序其实无所谓，但原则上要在公共 Library 初始化之后！次序 vs 顺序：次序 ~= 先后顺序。
// 		self::__constructI18N();
		
		$directory = $this->router->fetch_directory(); // eg: "manage/" or null
		$class = $this->router->fetch_class();
		$method = $this->router->fetch_method();
		
		// 前触发一般包括：
		// 登录验证
		// 权限验证
		$this->nowApp = $directory;
		$this->nowCtl = $class;
		$this->nowAct = "$class/$method";
		
// 		// 这二者需要在 $this->nowApp $this->nowCtl $this->nowAct 正确赋值后才能调用！
// 		self::__constructPassport();
// 		self::__constructPermission();
// 		// 这个需要在 self::__constructPassport() 之后调用，因为里面有 header.logined_member 的引用！
// 		self::__constructView();
	}
	
	
}

// MARK 以下子类控制器，在该文件中子类控制器只能在基类的定义之后定义

/**
 * 用户控制器
 *
 * @desc    功能包括多语言国际化、登录、视图
 * @version 1.0.0
 * @author  Jason
 * @date	2017年6月22日 下午1:49:39
 */
class MY_Consumer_Controller extends MY_Controller
{
	public function __construct()
	{
		parent::__construct();
		self::__constructI18N();
		self::__constructPassport();
		self::__constructView();
	}
}

/**
 * 商户控制器
 * 
 * @desc    功能包括多语言国际化、登录、//权限、视图
 * @version 1.0.0 
 * @author  Jason
 * @date    2017年6月21日 下午12:49:03
 */
class MY_Business_Controller extends MY_Controller
{
	public function __construct()
	{
		parent::__construct();
		self::__constructI18N();
		self::__constructPassport([
				'cookie_name' => $this->config->item('business_session_key'),
		]);
// 		self::__constructPermission();
		self::__constructView();
	}
	
	/**
	 * 检查商户是否登录，重写 Passport_Trait::check_login() 方法
	 * 
	 * @desc    源 MY_Controller::check_business_login() 方法。同样一个检查登录的功能，如果拆分开到子类里面，则靠覆盖。
	 * @author  Jason
	 * @date    2017年6月21日 下午3:25:50
	 * 
	 * @return  boolean	已登录返回真，未登录返回假。
	 */
	protected function check_login()
	{
		if (!$this->has_member_login()) {
			return false;
		}
		
		$member = $this->get_logined_member();
		if (empty($member['is_merchant'])) {
			return false;
		}
		
		return true;
	}
	
}

/**
 * 管理员控制器
 *
 * @desc    功能包括多语言国际化、登录、权限、视图
 * @version 1.0.0
 * @author  Jason
 * @date    2017年6月21日 下午12:55:03
 */
class MY_Manager_Controller extends MY_Controller
{
	public function __construct()
	{
		parent::__construct();
		self::__constructI18N();
		self::__constructPassport([
				'cookie_name' => $this->config->item('manager_session_key'),
		]);
		self::__constructPermission();
		self::__constructView();
	}
	
	/**
	 * 检查管理员是否登录，重写 Passport_Trait::check_login() 方法
	 *
	 * @desc    源 MY_Controller::check_manager_login() 方法。同样一个检查登录的功能，如果拆分开到子类里面，则靠覆盖。
	 * @author  Jason
	 * @date    2017年6月21日 下午3:25:50
	 *
	 * @return  boolean	已登录返回真，未登录返回假。
	 */
	protected function check_login()
	{
		if (!$this->has_member_login()) {
			return false;
		}
		
		$member = $this->get_logined_member();
		if (empty($member['is_manager'])) {
			return false;
		}
		
		return true;
	}
	
}